﻿using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using Ast.Core.Settings;
using Ast.Core.WinApi.DbgHelp;
using Ast.Framework;
using Castle.Core.Logging;

namespace Ast.Core.Services
{
    public interface IDbgHelpService : ILockable, ISingletonDependency
    {
        DbgHelpConstant.SymOption SymGetOptions();
        DbgHelpConstant.SymOption SymSetOptions(DbgHelpConstant.SymOption symOptions);
        bool SymInitialize();
        bool SymCleanup();

        string SymGetSearchPath();
        bool SymSetSearchPath(string searchPath);

        ulong SymLoadModule64(string imageName, string moduleName, ulong baseOfDll);
        ulong SymLoadModuleEx(string imageName, string moduleName, ulong baseOfDll);
        bool SymUnloadModule64(ulong baseOfDll);

        bool SymEnumSymbols(ulong baseOfDll, string mask, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback);
        bool SymEnumSymbolsForAddr(ulong address, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback);
        bool SymEnumerateModules64(DbgHelpNative.SymEnumerateModulesProc64 enumModulesCallback);
        bool SymFromName(string name, ref DbgHelpStructure.SymbolInfo symbol);
        bool SymFromAddr(ulong address, ref DbgHelpStructure.SymbolInfo symbol);
        bool SymFromIndex(ulong baseOfDll, uint index, ref DbgHelpStructure.SymbolInfo symbol);
        bool SymGetModuleInfo64(ulong address, ref DbgHelpStructure.ImagehlpModule64 moduleInfo);
        bool EnumerateLoadedModules64(DbgHelpNative.EnumerateLoadedModulesProc64 enumLoadedModulesCallback);
        bool SymRegisterCallback64(DbgHelpNative.SymRegisterCallbackProc64 callbackFunction);

        bool SymGetTypeFromName(ulong baseOfDll, string name, ref DbgHelpStructure.SymbolInfo symbol);
        bool SymGetTypeInfo(ulong moduleBase, uint typeId, DbgHelpConstant.ImagehlpSymbolTypeInfo getType, IntPtr info);

        bool SymSearch(ulong baseOfDll, uint index, DbgHelpConstant.SymTagEnum symTag, string mask, ulong address, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback);
        bool SymEnumTypes(ulong baseOfDll, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback);

        bool SymSetContext(ref DbgHelpStructure.ImagehlpStackFrame stackFrame);

        string UnDecorateSymbolName(string decoratedName, DbgHelpConstant.SymUndecorateOption flags);
    }

    public class DbgHelpService : IDbgHelpService, IDisposable
    {
        private readonly IAstSettings _astSettings;
        private readonly IntPtr _curProcessHandler;

        private readonly object _syncRoot = new object();
        private bool _isInitialize;

        public DbgHelpService(IAstSettings astSettings)
        {
            _astSettings = astSettings;
            _curProcessHandler = Process.GetCurrentProcess().Handle;

            SymInitialize();

            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        public DbgHelpConstant.SymOption SymGetOptions()
        {
            return DbgHelpNative.SymGetOptions();
        }

        public DbgHelpConstant.SymOption SymSetOptions(DbgHelpConstant.SymOption symOptions)
        {
            return DbgHelpNative.SymSetOptions(symOptions);
        }

        public bool SymInitialize()
        {
            if (_isInitialize) return false;
            if (DbgHelpNative.SymInitialize(_curProcessHandler, _astSettings.SymbolPath, false))
            {
                _isInitialize = true;
                return true;
            }
            Logger.Win32ErrorFormat("error occurred in SymInitialize");
            return false;
        }

        public bool SymCleanup()
        {
            if (!_isInitialize) return false;
            if (DbgHelpNative.SymCleanup(_curProcessHandler))
            {
                _isInitialize = false;
                return true;
            }
            Logger.Win32ErrorFormat("error occurred in SymCleanup");
            return false;
        }

        public string SymGetSearchPath()
        {
            var sb = new StringBuilder(1024);
            if (DbgHelpNative.SymGetSearchPath(_curProcessHandler, sb, sb.Capacity)) return sb.ToString();
            Logger.Win32ErrorFormat("error occurred in SymGetSearchPath");
            return null;
        }

        public bool SymSetSearchPath(string searchPath)
        {
            if (DbgHelpNative.SymSetSearchPath(_curProcessHandler, searchPath)) return true;
            Logger.Win32ErrorFormat("error occurred in SymGetSearchPath");
            return false;
        }

        public ulong SymLoadModule64(string imageName, string moduleName, ulong baseOfDll)
        {
            var retVal = DbgHelpNative.SymLoadModule64(_curProcessHandler, IntPtr.Zero, imageName, moduleName, baseOfDll, 0);
            if (retVal == 0)
            {
                Logger.Win32ErrorFormat("error occurred in SymLoadModule64");
            }
            return retVal;
        }

        public ulong SymLoadModuleEx(string imageName, string moduleName, ulong baseOfDll)
        {
            var dllSize = 0;
            if (".pdb".Equals(Path.GetExtension(imageName), StringComparison.InvariantCultureIgnoreCase))
            {
                dllSize = (int) new FileInfo(imageName).Length;
            }
            var retVal = DbgHelpNative.SymLoadModuleEx(_curProcessHandler, IntPtr.Zero, imageName, moduleName, baseOfDll, dllSize, IntPtr.Zero, 0);
            if (retVal == 0)
            {
                Logger.Win32ErrorFormat("error occurred in SymLoadModuleEx");
            }
            return retVal;
        }

        public bool SymUnloadModule64(ulong baseOfDll)
        {
            if (DbgHelpNative.SymUnloadModule64(_curProcessHandler, baseOfDll)) return true;
            Logger.Win32ErrorFormat("error occurred in SymUnloadModule64");
            return false;
        }

        public bool SymEnumSymbols(ulong baseOfDll, string mask, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback)
        {
            if (DbgHelpNative.SymEnumSymbols(_curProcessHandler, baseOfDll, mask, enumSymbolsCallback, IntPtr.Zero)) return true;
            Logger.Win32ErrorFormat("error occurred in SymEnumSymbols");
            return false;
        }

        public bool SymEnumSymbolsForAddr(ulong address, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback)
        {
            if (DbgHelpNative.SymEnumSymbolsForAddr(_curProcessHandler, address, enumSymbolsCallback, IntPtr.Zero)) return true;
            Logger.Win32ErrorFormat("error occurred in SymEnumSymbolsForAddr");
            return false;
        }

        public bool SymEnumerateModules64(DbgHelpNative.SymEnumerateModulesProc64 enumModulesCallback)
        {
            if (DbgHelpNative.SymEnumerateModules64(_curProcessHandler, enumModulesCallback, IntPtr.Zero)) return true;
            Logger.Win32ErrorFormat("error occurred in SymEnumerateModules64");
            return false;
        }

        public bool SymFromName(string name, ref DbgHelpStructure.SymbolInfo symbol)
        {
            if (DbgHelpNative.SymFromName(_curProcessHandler, name, ref symbol)) return true;
            Logger.Win32ErrorFormat("error occurred in SymFromName");
            return false;
        }

        public bool SymFromAddr(ulong address, ref DbgHelpStructure.SymbolInfo symbol)
        {
            ulong displacement = 0;
            if (DbgHelpNative.SymFromAddr(_curProcessHandler, address, ref displacement, ref symbol)) return true;
            Logger.Win32ErrorFormat("error occurred in SymFromAddr");
            return false;
        }

        public bool SymFromIndex(ulong baseOfDll, uint index, ref DbgHelpStructure.SymbolInfo symbol)
        {
            if (DbgHelpNative.SymFromIndex(_curProcessHandler, baseOfDll, index, ref symbol)) return true;
            Logger.Win32ErrorFormat("error occurred in SymFromIndex");
            return false;
        }

        public bool SymGetModuleInfo64(ulong address, ref DbgHelpStructure.ImagehlpModule64 moduleInfo)
        {
            if (DbgHelpNative.SymGetModuleInfo64(_curProcessHandler, address, ref moduleInfo)) return true;
            Logger.Win32ErrorFormat("error occurred in SymGetModuleInfo64");
            return false;
        }

        public bool EnumerateLoadedModules64(DbgHelpNative.EnumerateLoadedModulesProc64 enumLoadedModulesCallback)
        {
            if (DbgHelpNative.EnumerateLoadedModules64(_curProcessHandler, enumLoadedModulesCallback, IntPtr.Zero)) return true;
            Logger.Win32ErrorFormat("error occurred in EnumerateLoadedModules64");
            return false;
        }

        public bool SymRegisterCallback64(DbgHelpNative.SymRegisterCallbackProc64 callbackFunction)
        {
            if (DbgHelpNative.SymRegisterCallback64(_curProcessHandler, callbackFunction, 0)) return true;
            Logger.Win32ErrorFormat("error occurred in SymRegisterCallback64");
            return false;
        }

        public bool SymGetTypeFromName(ulong baseOfDll, string name, ref DbgHelpStructure.SymbolInfo symbol)
        {
            if (DbgHelpNative.SymGetTypeFromName(_curProcessHandler, baseOfDll, name, ref symbol)) return true;
            Logger.Win32ErrorFormat("error occurred in SymGetTypeFromName");
            return false;
        }

        public bool SymGetTypeInfo(ulong moduleBase, uint typeId, DbgHelpConstant.ImagehlpSymbolTypeInfo getType, IntPtr info)
        {
            if (DbgHelpNative.SymGetTypeInfo(_curProcessHandler, moduleBase, typeId, getType, info)) return true;
            Logger.Win32ErrorFormat("error occurred in SymGetTypeInfo");
            return false;
        }

        public bool SymSearch(ulong baseOfDll, uint index, DbgHelpConstant.SymTagEnum symTag, string mask, ulong address, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback)
        {
            if (DbgHelpNative.SymSearch(_curProcessHandler, baseOfDll, index, symTag, mask, address, enumSymbolsCallback, IntPtr.Zero, DbgHelpConstant.SymSearchOption.SYMSEARCH_ALLITEMS)) return true;
            Logger.Win32ErrorFormat("error occurred in SymSearch");
            return false;
        }

        public bool SymEnumTypes(ulong baseOfDll, DbgHelpNative.SymEnumSymbolsProc enumSymbolsCallback)
        {
            if (DbgHelpNative.SymEnumTypes(_curProcessHandler, baseOfDll, enumSymbolsCallback, IntPtr.Zero)) return true;
            Logger.Win32ErrorFormat("error occurred in SymEnumTypes");
            return false;
        }

        public bool SymSetContext(ref DbgHelpStructure.ImagehlpStackFrame stackFrame)
        {
            if (DbgHelpNative.SymSetContext(_curProcessHandler, ref stackFrame, IntPtr.Zero)) return true;
            Logger.Win32ErrorFormat("error occurred in SymSetContext");
            return false;
        }

        public string UnDecorateSymbolName(string decoratedName, DbgHelpConstant.SymUndecorateOption flags)
        {
            var capacity = 256;
            while (true)
            {
                var sb = new StringBuilder(capacity);
                if (DbgHelpNative.UnDecorateSymbolName(decoratedName, sb, capacity, flags) > 0)
                {
                    return sb.ToString();
                }
                if (capacity > 1024)
                {
                    Logger.Win32ErrorFormat("error occurred in UnDecorateSymbolName");
                    return null;
                }
                capacity += 64;
            }
        }

        public void AcquireLock()
        {
            Monitor.Enter(_syncRoot);
        }

        public void ReleaseLock()
        {
            Monitor.Exit(_syncRoot);
        }

        public void Dispose()
        {
            AcquireLock();
            if (SymCleanup())
            {
                Logger.InfoFormat("Cleaned up symbols");
            }
            ReleaseLock();
        }
    }
}